﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Security.Cryptography;
using System.IO;

namespace McSherry.SXM.Base.FileFormat
{
    /// <summary>
    /// An enum representing the quarters of the year in which a program was released.
    /// </summary>
    public enum SXQuarter {
        FirstQuarter, SecondQuarter,
        ThirdQuarter, FourthQuarter
    }

    /// <summary>
    /// An enum representing the development stages a program is in.
    /// </summary>
    public enum SXDevStage {
        Alpha = 0xA, Beta = 0xB, PreRelease = -0xF,
        Release = 0xF, Stable = 0xFF, Mature = -0xFF,
        EndOfLife = -0x0
    }

    /// <summary>
    /// A class for writing SX files.
    /// </summary>
    public class SXWriter
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="AuthorInfo">The author information to use when writing.</param>
        /// <param name="ProgramInfo">The program information to use when writing.</param>
        /// <param name="DeveloperKey">The developer key to use when writing. If no key is to be used, use DeveloperKey.NullKey.</param>
        public SXWriter(SXAuthor AuthorInfo, SXProgram ProgramInfo, DeveloperKey DeveloperKey)
        {
            this.AuthorInfo = AuthorInfo;
            this.ProgramInfo = ProgramInfo;
            this.DevKey = DeveloperKey;
        }

        /// <summary>
        /// The author information to be used when writing.
        /// </summary>
        public SXAuthor AuthorInfo;
        /// <summary>
        /// The program information to be used when writing.
        /// </summary>
        public SXProgram ProgramInfo;
        /// <summary>
        /// The developer key to be used when writing.
        /// </summary>
        public DeveloperKey DevKey;

        /// <summary>
        /// Write the provided information to an SX file at the specified path.
        /// </summary>
        /// <param name="path">The path to write to. This should not include a file name, as that will be automatically appended.</param>
        /// <example>SXWriter.Write("C:/Extensions/");</example>
        public void Write(string path)
        {
            string xml =
                "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                "<sx xmlns=\"http://mcsherry.koding.com\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://mcsherry.koding.com sxm/SX12-A.xsd\">" +
                    "<format version=\"SX:12-A\"/>" +
                    AuthorInfo.ToString() +
                    ProgramInfo.ToString() +
                "</sx>"
                ;

            StreamWriter xmw = new StreamWriter(path +
                Convert.ToBase64String(ProgramInfo.GenerateExtensionHashMD5()) + ".sx");
            byte[] bytes = Encoding.ASCII.GetBytes(xml);

            if (DevKey.Key != null)
            {
                AES aes = new AES(DevKey);
                bytes = aes.Encrypt(aes.ByteArrToString(bytes));
            }

            xmw.Write(Convert.ToBase64String(bytes, Base64FormattingOptions.InsertLineBreaks));
            xmw.Close();
            xmw.Dispose();
        }
    }

    /// <summary>
    /// A class for reading SX files.
    /// </summary>
    public class SXReader
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="path">The path of the SX file to read.</param>
        /// <param name="dk">The developer key to use when reading. If no key is to be used, use DeveloperKey.NullKey.</param>
        public SXReader(string path, DeveloperKey dk) { this.FilePath = path; this.DevKey = dk; }

        private string FilePath;
        private DeveloperKey DevKey;

        /// <summary>
        /// Read the full contents of the specified SX files.
        /// </summary>
        /// <returns>An SXFile class containing the information contained in the SX file.</returns>
        /// <exception cref="System.Xml.XmlException">Thrown when the file format provided in the SX file is unrecognised.</exception>
        /// <exception cref="System.Xml.XmlException">Thrown when the XML in the file is not readable, likely because it is encrypted.</exception>
        public SXFile ReadToEnd()
        {
            SXFile retf = new SXFile();

            string contents = File.ReadAllText(FilePath);
            byte[] bytes = Convert.FromBase64String(contents);

            if (DevKey != DeveloperKey.NullKey)
            {
                AES aes = new AES(DevKey);
                contents = aes.Decrypt(bytes);
            }
            else contents = Encoding.UTF8.GetString(bytes);

            XmlDocument xmd = new XmlDocument();
            xmd.LoadXml(contents);

            XmlElement root = xmd.DocumentElement;
            XmlNode format = root.ChildNodes[0];

            if (format.Attributes["version"].Value == "SX:12-A")
            {
                XmlNode author = root.ChildNodes[1];
                XmlNode program = root.ChildNodes[2];

                retf.Author = new SXAuthor()
                {
                    Name = author.ChildNodes[0].InnerText,
                    Email = author.ChildNodes[1].InnerText,
                    Company = author.ChildNodes[2].ChildNodes[0].InnerText,
                    Copyright = author.ChildNodes[2].ChildNodes[1].InnerText,
                    Website = new Uri(author.ChildNodes[2].ChildNodes[2].InnerText)
                };

                retf.Program = new SXProgram()
                {
                    Name = program.ChildNodes[0].InnerText,
                    VersionString = new SXRevision(
                        ushort.Parse(program.ChildNodes[1].InnerText.Split('.')[0]),
                        ushort.Parse(program.ChildNodes[1].InnerText.Split('.')[1]),
                        uint.Parse(program.ChildNodes[1].InnerText.Split('.')[2].Remove(program.ChildNodes[1].InnerText.Split('.')[2].Length - 3)),
                        StringToQuarter(program.ChildNodes[1].InnerText.Substring(program.ChildNodes[1].InnerText.Length - 2))),
                    ReleaseDate = DateTime.ParseExact(program.ChildNodes[2].InnerText, "dd/MM/yyyy", System.Globalization.CultureInfo.InvariantCulture),
                    DevelopmentStage = StringToDStage(program.ChildNodes[3].InnerText),
                    DocumentationFile = program.ChildNodes[4].ChildNodes[1].ChildNodes[1].InnerText,
                    ExtensionFile = program.ChildNodes[4].ChildNodes[0].ChildNodes[1].InnerText
                    
                };
                retf.ExtensionFileHash = program.ChildNodes[4].ChildNodes[0].ChildNodes[0].InnerText;
                retf.DocumentationFileHash = program.ChildNodes[4].ChildNodes[1].ChildNodes[0].InnerText;
            }
            else throw new XmlException("Unrecognised SX Format \"" + format.Attributes[0].Value + "\"");

            return retf;
        }

        /// <summary>
        /// Converts a string to an SXQuarter enum value.
        /// </summary>
        /// <param name="str">The string representing a quarter, in the following format: Q1</param>
        /// <returns>An SXQuarter enum value representing the specified quarter.</returns>
        /// <exception cref="System.ArgumentException">Thrown when the specified string is not in the specified format.</exception>
        public static SXQuarter StringToQuarter(string str)
        {
            switch (str.ToLower())
            {
                case "q1": return SXQuarter.FirstQuarter;
                case "q2": return SXQuarter.SecondQuarter;
                case "q3": return SXQuarter.ThirdQuarter;
                case "q4": return SXQuarter.FourthQuarter;
                default: throw new ArgumentException("Invalid value provided for release quarter.");
            }
        }
        /// <summary>
        /// Converts a string to a development stage.
        /// </summary>
        /// <param name="str">The string to convert to a development stage.</param>
        /// <returns>An SXDevStage enum value representing the development stage.</returns>
        /// <exception cref="System.ArgumentException">Thrown when the provided string could not be converted to an SXDevStage value.</exception>
        public static SXDevStage StringToDStage(string str)
        {
            switch (str.ToLower())
            {
                case "alpha": return SXDevStage.Alpha;
                case "beta": return SXDevStage.Beta;
                case "prerelease": case "pre-release": return SXDevStage.PreRelease;
                case "release": return SXDevStage.Release;
                case "stable": return SXDevStage.Stable;
                case "mature": return SXDevStage.Mature;
                case "endoflife": case "end-of-life": case "eol": return SXDevStage.EndOfLife;
                default: throw new ArgumentException("Invalid value for development stage.");
            }
        }
    }

    /// <summary>
    /// Represents a revision number for an extension.
    /// </summary>
    public struct SXRevision
    {
        /// <summary>
        /// The major version of the extension. For example, 1.0.0, where one is the major version.
        /// </summary>
        public ushort MajorVersion;
        /// <summary>
        /// The minor version of the extension. For example, 0.1.0, where 1 is the minor version.
        /// </summary>
        public ushort MinorVersion;
        /// <summary>
        /// The number of times the extension has been built. For example, 0.0.1, where one is the build number.
        /// </summary>
        public uint BuildNumber;
        /// <summary>
        /// The quarter of the year that the current version was released in.
        /// </summary>
        public SXQuarter ReleaseQuarter;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="MajorVersion">The major revision. For example, 1.0.0 where 1 is the major revision.</param>
        /// <param name="MinorVersion">The minor revision. For example, 0.1.0 where 1 is the minor revision.</param>
        /// <param name="BuildNumber">The build number. For example, 0.0.1 where 1 is the build number.</param>
        /// <param name="ReleaseQuarter">The release quarter. For example, 0.0.0:Q4 where Q4 represents the fourth quarter.</param>
        public SXRevision(ushort MajorVersion, ushort MinorVersion, uint BuildNumber, SXQuarter ReleaseQuarter)
        {
            this.MajorVersion = MajorVersion;
            this.MinorVersion = MinorVersion;
            this.BuildNumber = BuildNumber;
            this.ReleaseQuarter = ReleaseQuarter;
        }

        private string _RQuarter_
        {
            get
            {
                switch (ReleaseQuarter)
                {
                    case SXQuarter.FirstQuarter:
                        return "Q1";
                    case SXQuarter.SecondQuarter:
                        return "Q2";
                    case SXQuarter.ThirdQuarter:
                        return "Q3";
                    case SXQuarter.FourthQuarter:
                        return "Q4";
                }
                return (string)null;
            }
        }

        public override string ToString()
        {
            return MajorVersion.ToString() + "." + MinorVersion.ToString() + "." +
                BuildNumber.ToString() + ":" + _RQuarter_;
        }
    }

    /// <summary>
    /// A struct representing information about a program author.
    /// </summary>
    public struct SXAuthor
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="Name">The full name of the author.</param>
        /// <param name="Email">The email address of the author.</param>
        /// <param name="Company">The company/organisation the author works for or represents.</param>
        /// <param name="Copyright">A copyright statement. For example, Copyright (c) [year] [name]</param>
        /// <param name="Website">The author's website.</param>
        /// <param name="DeveloperKey">A byte array representing a developer key. Not used at the moment, but reserved.</param>
        public SXAuthor
            (
                string Name, string Email, string Company,
                string Copyright, Uri Website, byte[] DeveloperKey
            )
        {
            this.Name = Name;
            this.Email = Email;
            this.Company = Company;
            this.Copyright = Copyright;
            this.Website = Website;
            this.DeveloperKey = DeveloperKey;
        }

        /// <summary>
        /// The name of the author.
        /// </summary>
        public string Name;
        /// <summary>
        /// The email address of the author.
        /// </summary>
        public string Email;
        /// <summary>
        /// The company/organisation the author works for or represents.
        /// </summary>
        public string Company;
        /// <summary>
        /// A copyright statement.
        /// </summary>
        /// <example>Copyright (c) 1997 John Doe</example>
        public string Copyright;
        /// <summary>
        /// The author's website.
        /// </summary>
        public Uri Website;
        /// <summary>
        /// A byte array representing a developer key. Currently not in use, but reserved.
        /// </summary>
        public byte[] DeveloperKey;

        /// <summary>
        /// Returns an XML representation of the author information.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return
                "<author>" +
                    "<name>" + this.Name + "</name>" +
                    "<email>" + this.Email + "</email>" +
                    "<company>" +
                        "<id>" + this.Company + "</id>" +
                        "<copyright>" + this.Copyright + "</copyright>" +
                        "<website>" + this.Website.AbsoluteUri + "</website>" +
                    "</company>" +
                "</author>"
                ;
        }
    }

    /// <summary>
    /// A struct representing information about a program.
    /// </summary>
    public struct SXProgram
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="Name">The name of the program.</param>
        /// <param name="ExtensionFile">The file path to the extension. If the SXProgram is returned from an SXReader, this contains a Base64 representation of the program.</param>
        /// <param name="DocumentationFile">The file path to the extension's documentation. If the SXProgram is returned from an SXReader, this contains a Base64 representation of the documentation file.</param>
        /// <param name="VersionString">The version string of the program.</param>
        /// <param name="ReleaseDate">The release date of the program.</param>
        /// <param name="DevelopmentStage">The development stage of the program.</param>
        public SXProgram(
            string Name, string ExtensionFile, string DocumentationFile,
            SXRevision VersionString, DateTime ReleaseDate, SXDevStage DevelopmentStage
            )
        {
            this.Name = Name;
            this.ExtensionFile = ExtensionFile;
            this.DocumentationFile = DocumentationFile;
            this.VersionString = VersionString;
            this.ReleaseDate = ReleaseDate;
            this.DevelopmentStage = DevelopmentStage;
        }

        /// <summary>
        /// The name of the program.
        /// </summary>
        public string Name;
        /// <summary>
        /// The development stage of the program.
        /// </summary>
        public SXDevStage DevelopmentStage;
        /// <summary>
        /// The path to the extension file. If this SXProgram was returned from an SXReader, this contains a Base64 representation of the extension file.
        /// </summary>
        public string ExtensionFile;
        /// <summary>
        /// The path to the extension's documentation file. If this SXProgram is returned from an SXReader, this contains a Base64 representation of the documentation file.
        /// </summary>
        public string DocumentationFile;
        /// <summary>
        /// The version string of the program.
        /// </summary>
        public SXRevision VersionString;
        /// <summary>
        /// The release date of the program.
        /// </summary>
        public DateTime ReleaseDate;

        /// <summary>
        /// Returns an XML representation of the program information.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return
                "<program>" +
                    "<name>" + this.Name + "</name>" +
                    "<version>" + this.VersionString + "</version>" +
                    "<release>" + this.ReleaseDate.ToShortDateString() + "</release>" +
                    "<devstage>" + this.DevelopmentStage + "</devstage>" +
                    "<extension>" +
                        "<library>" +
                            "<hash>" + Convert.ToBase64String(this.GenerateExtensionHashSHA256()) + "</hash>" +
                            "<file>" + Convert.ToBase64String(File.ReadAllBytes(this.ExtensionFile)) + "</file>" +
                        "</library>" +
                        "<document>" + 
                            "<hash>" + Convert.ToBase64String(this.GenerateDocumentHashSHA256()) + "</hash>" +
                            "<file>" + Convert.ToBase64String(File.ReadAllBytes(this.DocumentationFile)) + "</file>" +
                        "</document>" +
                    "</extension>" +
                "</program>"
                ;
        }

        /// <summary>
        /// Generates a hash of the extension file using the RIPEMD160 algorithm.
        /// </summary>
        /// <returns></returns>
        internal byte[] GenerateExtensionHashRIPEMD160()
        {
            HMACRIPEMD160 hm160 = new HMACRIPEMD160(File.ReadAllBytes(this.ExtensionFile));
            return hm160
                    .ComputeHash(File
                        .ReadAllBytes(this.ExtensionFile));
        }
        /// <summary>
        /// Generates a hash of the extension file using the SHA2 256-bit algorithm.
        /// </summary>
        /// <returns></returns>
        internal byte[] GenerateExtensionHashSHA256()
        {
            HMACSHA256 sha256 = new HMACSHA256(File.ReadAllBytes(this.ExtensionFile));
            return sha256
                    .ComputeHash(File
                        .ReadAllBytes(this.ExtensionFile
                        ));
        }
        /// <summary>
        /// Generates a hash of the extension file using the MD5 algorithm.
        /// </summary>
        /// <returns></returns>
        internal byte[] GenerateExtensionHashMD5()
        {
            HMACMD5 md5 = new HMACMD5(File.ReadAllBytes(this.ExtensionFile));
            return md5
                    .ComputeHash(File
                        .ReadAllBytes(this.ExtensionFile));
        }
        /// <summary>
        /// Generates a hash of the extension documentation using the RIPEMD160 algorithm.
        /// </summary>
        /// <returns></returns>
        internal byte[] GenerateDocumentHashRIPEMD160()
        {
            HMACRIPEMD160 hm160 = new HMACRIPEMD160(File.ReadAllBytes(this.DocumentationFile));
            return hm160
                    .ComputeHash(File
                        .ReadAllBytes(this.DocumentationFile));
        }
        /// <summary>
        /// Generates a hash of the extension documentation using the SHA2 256-bit algorithm.
        /// </summary>
        /// <returns></returns>
        internal byte[] GenerateDocumentHashSHA256()
        {
            HMACSHA256 sha256 = new HMACSHA256(File.ReadAllBytes(this.DocumentationFile));
            return sha256
                    .ComputeHash(File
                        .ReadAllBytes(this.DocumentationFile
                        ));
        }
        /// <summary>
        /// Generates a hash of the extension's documentation using the MD5 algorithm.
        /// </summary>
        /// <returns></returns>
        internal byte[] GenerateDocumentHashMD5()
        {
            HMACMD5 md5 = new HMACMD5(File.ReadAllBytes(this.DocumentationFile));
            return md5
                    .ComputeHash(File
                        .ReadAllBytes(this.DocumentationFile));
        }
    }

    /// <summary>
    /// A struct that converts a byte or a byte array to a string representation.
    /// </summary>
    public struct ByteString
    {
        /// <summary>
        /// The bytes to convert to a string.
        /// </summary>
        public byte[] Bytes;
        /// <summary>
        /// The byte to convert to a string.
        /// </summary>
        public byte Byte;
        private bool byteArray;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="Bytes">The bytes to convert to a string.</param>
        public ByteString(byte[] Bytes) 
        { 
            this.Bytes = Bytes;
            this.Byte = 0x00;
            this.byteArray = true;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="Byte">The byte to convert to a string.</param>
        public ByteString(byte Byte) 
        { 
            this.Byte = Byte; 
            this.Bytes = new byte[] { 0x00 };
            this.byteArray = false;
        }

        /// <summary>
        /// Convert the byte(s) into their string representation.
        /// </summary>
        /// <returns>An ASCII-encoded string.</returns>
        public override string ToString()
        {
            return (byteArray ? Encoding.ASCII.GetString(Bytes) : Encoding.ASCII.GetString(new byte[] { Byte }));
        }
        /// <summary>
        /// Convert the byte(s) into their string representation.
        /// </summary>
        /// <param name="encoder">The encoding to use when converting.</param>
        /// <returns>A string encoded with the specified encoder.</returns>
        public string ToEncodedString(Encoding encoder)
        {
            return (byteArray ? encoder.GetString(Bytes) : encoder.GetString(new byte[] { Byte }));
        }
        /// <summary>
        /// Convert the byte(s) into their string representation.
        /// </summary>
        /// <returns>A Base64-encoded string.</returns>
        public string ToBase64String()
        {
            return (byteArray ? Convert.ToBase64String(Bytes) : Convert.ToBase64String(new byte[] { Byte }));
        }
    }

    /// <summary>
    /// A class representing the contents of an SX file.
    /// </summary>
    public class SXFile
    {
        public SXFile() { }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="Author">The author information to store.</param>
        /// <param name="Program">The program information to store.</param>
        public SXFile(SXAuthor Author, SXProgram Program)
        {
            this.Author = Author;
            this.Program = Program;
        }

        /// <summary>
        /// The author information stored in the SX file.
        /// </summary>
        public SXAuthor Author;
        /// <summary>
        /// The program information stored in the SX file.
        /// </summary>
        public SXProgram Program;

        /// <summary>
        /// Represents a hash of the extension file. Algorithm varies depending on implementation of SXReader.
        /// </summary>
        public string ExtensionFileHash;
        /// <summary>
        /// Represents a hash of the extension's documentation file. Algorithm varies depending on implementation of SXReader.
        /// </summary>
        public string DocumentationFileHash;
    }
}
